/*
* @file gkt_date_time.c
*/
#include "gkt_debug.h"
#include "gkt_date_time.h"

#ifdef GKT_CONFIG_YEAR_MAX
#define DT_YEAR_MAX	GKT_CONFIG_DT_YEAR_MAX
#else
#define DT_YEAR_MAX	100
#endif

#ifdef GKT_CONFIG_DT_TIME_ZONE
#define DT_TIME_ZONE	GKT_CONFIG_DT_TIME_ZONE
#else
#define DT_TIME_ZONE	8
#endif

#define DT_NORMAL_NIL_DAYS	0
#define DT_NORMAL_JAN_DAYS	31
#define DT_NORMAL_FEB_DAYS	28
#define DT_NORMAL_MAR_DAYS	31
#define DT_NORMAL_APR_DAYS	30
#define DT_NORMAL_MAY_DAYS	31
#define DT_NORMAL_JUN_DAYS	30
#define DT_NORMAL_JUL_DAYS	31
#define DT_NORMAL_AUG_DAYS	31
#define DT_NORMAL_SEP_DAYS	30
#define DT_NORMAL_OCT_DAYS	31
#define DT_NORMAL_NOV_DAYS	30
#define DT_NORMAL_DEC_DAYS	31
static const uint8_t sc_dt_month_days[2][13] = {
	{	/* normal year */
		DT_NORMAL_NIL_DAYS,
		DT_NORMAL_JAN_DAYS,
		DT_NORMAL_FEB_DAYS,
		DT_NORMAL_MAR_DAYS,
		DT_NORMAL_APR_DAYS,
		DT_NORMAL_MAY_DAYS,
		DT_NORMAL_JUN_DAYS,
		DT_NORMAL_JUL_DAYS,
		DT_NORMAL_AUG_DAYS,
		DT_NORMAL_SEP_DAYS,
		DT_NORMAL_OCT_DAYS,
		DT_NORMAL_NOV_DAYS,
		DT_NORMAL_DEC_DAYS
	},
	{	/* leap year */
		DT_NORMAL_NIL_DAYS,
		DT_NORMAL_JAN_DAYS,
		DT_NORMAL_FEB_DAYS + 1,
		DT_NORMAL_MAR_DAYS,
		DT_NORMAL_APR_DAYS,
		DT_NORMAL_MAY_DAYS,
		DT_NORMAL_JUN_DAYS,
		DT_NORMAL_JUL_DAYS,
		DT_NORMAL_AUG_DAYS,
		DT_NORMAL_SEP_DAYS,
		DT_NORMAL_OCT_DAYS,
		DT_NORMAL_NOV_DAYS,
		DT_NORMAL_DEC_DAYS
	}
};

#define DT_NORMAL_NIL_ACCUMULATED_DAYS	0
#define DT_NORMAL_JAN_ACCUMULATED_DAYS	(DT_NORMAL_NIL_ACCUMULATED_DAYS + DT_NORMAL_JAN_DAYS)
#define DT_NORMAL_FEB_ACCUMULATED_DAYS	(DT_NORMAL_JAN_ACCUMULATED_DAYS + DT_NORMAL_FEB_DAYS)
#define DT_NORMAL_MAR_ACCUMULATED_DAYS	(DT_NORMAL_FEB_ACCUMULATED_DAYS + DT_NORMAL_MAR_DAYS)
#define DT_NORMAL_APR_ACCUMULATED_DAYS	(DT_NORMAL_MAR_ACCUMULATED_DAYS + DT_NORMAL_APR_DAYS)
#define DT_NORMAL_MAY_ACCUMULATED_DAYS	(DT_NORMAL_APR_ACCUMULATED_DAYS + DT_NORMAL_MAY_DAYS)
#define DT_NORMAL_JUN_ACCUMULATED_DAYS	(DT_NORMAL_MAY_ACCUMULATED_DAYS + DT_NORMAL_JUN_DAYS)
#define DT_NORMAL_JUL_ACCUMULATED_DAYS	(DT_NORMAL_JUN_ACCUMULATED_DAYS + DT_NORMAL_JUL_DAYS)
#define DT_NORMAL_AUG_ACCUMULATED_DAYS	(DT_NORMAL_JUL_ACCUMULATED_DAYS + DT_NORMAL_AUG_DAYS)
#define DT_NORMAL_SEP_ACCUMULATED_DAYS	(DT_NORMAL_AUG_ACCUMULATED_DAYS + DT_NORMAL_SEP_DAYS)
#define DT_NORMAL_OCT_ACCUMULATED_DAYS	(DT_NORMAL_SEP_ACCUMULATED_DAYS + DT_NORMAL_OCT_DAYS)
#define DT_NORMAL_NOV_ACCUMULATED_DAYS	(DT_NORMAL_OCT_ACCUMULATED_DAYS + DT_NORMAL_NOV_DAYS)
#define DT_NORMAL_DEC_ACCUMULATED_DAYS	(DT_NORMAL_NOV_ACCUMULATED_DAYS + DT_NORMAL_DEC_DAYS)
static const uint16_t sc_dt_accumulated_days[2][13] = {
	{
		DT_NORMAL_NIL_ACCUMULATED_DAYS,
		DT_NORMAL_JAN_ACCUMULATED_DAYS,
		DT_NORMAL_FEB_ACCUMULATED_DAYS,
		DT_NORMAL_MAR_ACCUMULATED_DAYS,
		DT_NORMAL_APR_ACCUMULATED_DAYS,
		DT_NORMAL_MAY_ACCUMULATED_DAYS,
		DT_NORMAL_JUN_ACCUMULATED_DAYS,
		DT_NORMAL_JUL_ACCUMULATED_DAYS,
		DT_NORMAL_AUG_ACCUMULATED_DAYS,
		DT_NORMAL_SEP_ACCUMULATED_DAYS,
		DT_NORMAL_OCT_ACCUMULATED_DAYS,
		DT_NORMAL_NOV_ACCUMULATED_DAYS,
		DT_NORMAL_DEC_ACCUMULATED_DAYS
	},
	{
		DT_NORMAL_NIL_ACCUMULATED_DAYS,
		DT_NORMAL_JAN_ACCUMULATED_DAYS,
		DT_NORMAL_FEB_ACCUMULATED_DAYS + 1,
		DT_NORMAL_MAR_ACCUMULATED_DAYS + 1,
		DT_NORMAL_APR_ACCUMULATED_DAYS + 1,
		DT_NORMAL_MAY_ACCUMULATED_DAYS + 1,
		DT_NORMAL_JUN_ACCUMULATED_DAYS + 1,
		DT_NORMAL_JUL_ACCUMULATED_DAYS + 1,
		DT_NORMAL_AUG_ACCUMULATED_DAYS + 1,
		DT_NORMAL_SEP_ACCUMULATED_DAYS + 1,
		DT_NORMAL_OCT_ACCUMULATED_DAYS + 1,
		DT_NORMAL_NOV_ACCUMULATED_DAYS + 1,
		DT_NORMAL_DEC_ACCUMULATED_DAYS + 1
	}
};

__WEAK int gkt_dt_check_date(const gkt_date_u *date)
{
	int leap_year;
	int result;

	if (date 
		&& (date->month >= GKT_DT_MONTH_JAN) 
		&& (date->month <= GKT_DT_MONTH_DEC)
		&& date->day)
	{
		leap_year = gkt_dt_leap_year(date->year);
		result = (date->day <= sc_dt_month_days[leap_year][date->month]) ? 1 : 0;
	}
	else
		result = 0;

	return result;
}

static void dt_add_days(gkt_date_u *date, 
				uint32_t days)
{
	uint32_t days_elapsed, days_left;
	int leap_year;

	leap_year = gkt_dt_leap_year(date->year);
	days_elapsed = sc_dt_accumulated_days[leap_year][date->month - 1] + date->day - 1;
	days_left = (leap_year ? GKT_DT_LEAP_YEAR_DAYS : GKT_DT_NORMAL_YEAR_DAYS) - days_elapsed;
	if (days_left <= days) {
		date->day = 1;
		date->month = GKT_DT_MONTH_JAN;
		date->year++;
		days -= days_left;
		if (!days)
			return;

		do {
			leap_year = gkt_dt_leap_year(date->year);
			days_left = leap_year ? GKT_DT_LEAP_YEAR_DAYS : GKT_DT_NORMAL_YEAR_DAYS;
			if (days_left <= days) {
				date->year++;
				days -= days_left;
				if (!days)
					return;
			}
			else
				break;
		} while(1);
	}

	do {
		days_left = sc_dt_month_days[leap_year][date->month] - date->day + 1;
		if (days_left <= days) {
			date->day = 1;
			date->month++;
			if (date->month > GKT_DT_MONTH_DEC) {
				date->month = GKT_DT_MONTH_JAN;
				date->year++;
				leap_year = gkt_dt_leap_year(date->year);
			}

			days -= days_left;
			if (!days)
				return;
		}
		else {
			date->day += (uint8_t)days;
			break;
		}
	} while (1);
}

static void dt_add_seconds(gkt_date_time_s *dt, 
				uint32_t seconds)
{
	uint32_t days, hours, minutes;

	days = seconds / GKT_DT_DAY_SECONDS;
	seconds %= GKT_DT_DAY_SECONDS;

	hours = seconds / GKT_DT_HOUR_SECONDS;
	seconds %= GKT_DT_HOUR_SECONDS;

	minutes = seconds / GKT_DT_MINUTE_SECONDS;
	seconds %= GKT_DT_MINUTE_SECONDS;

	dt->time.second += (uint8_t)seconds;
	if (dt->time.second >= GKT_DT_MINUTE_SECONDS) {
		dt->time.second -= GKT_DT_MINUTE_SECONDS;
		minutes++;
	}

	dt->time.minute += (uint8_t)minutes;
	if (dt->time.minute >= GKT_DT_HOUR_MINUTES) {
		dt->time.minute -= GKT_DT_HOUR_MINUTES;
		hours++;
	}

	dt->time.hour += (uint8_t)hours;
	if (dt->time.hour >= GKT_DT_DAY_HOURS) {
		dt->time.hour -= GKT_DT_DAY_HOURS;
		days++;
	}

	if (days)
		dt_add_days(&dt->date, days);
}

__WEAK void gkt_dt_add_seconds(gkt_date_time_s *dt, uint32_t seconds)
{
	if (dt && gkt_dt_check_date(&dt->date) 
		&& gkt_dt_check_time(&dt->time))
	{
		dt_add_seconds(dt, seconds);
	}
}

__WEAK void gkt_dt_add_days(gkt_date_u *date, 
				uint32_t days)
{
	if (date && gkt_dt_check_date(date))
		dt_add_days(date, days);
}


static uint32_t dt_seconds_elapsed_of_day(const gkt_time_u *time)
{
	uint32_t seconds;

	seconds = time->hour * GKT_DT_HOUR_SECONDS;
	seconds += time->minute * GKT_DT_MINUTE_SECONDS;
	seconds += time->second;

	return seconds;
}

static uint32_t dt_days_elapsed_of_year(const gkt_date_u *date)
{
	int leap_year;
	uint32_t days;

	leap_year = gkt_dt_leap_year(date->year);
	days = sc_dt_accumulated_days[leap_year][date->month - GKT_DT_MONTH_JAN];
	days += (uint32_t)(date->day - 1);

	return days;
}

static uint32_t dt_days_elapsed_from_base(const gkt_date_u *date, 
					const gkt_date_u *date_base)
{
	int leap_year;
	uint32_t year, days_elapsed;

	days_elapsed = 0;
	if (date_base) {
		year = (uint32_t)date_base->year;
		days_elapsed -= dt_days_elapsed_of_year(date_base);
	}
	else
		year = GKT_DT_YEAR_BASE;

	while (year < (uint32_t)date->year) {
		leap_year = gkt_dt_leap_year(year);
		days_elapsed += leap_year ? GKT_DT_LEAP_YEAR_DAYS : GKT_DT_NORMAL_YEAR_DAYS;
		year++;
	}
	days_elapsed += dt_days_elapsed_of_year(date);

	return days_elapsed;
}

int gkt_dt_days_elapsed_from_base(const gkt_date_u *date, 
					const gkt_date_u *date_base)
{
	if (date && gkt_dt_check_date(date)
		&& date_base && gkt_dt_check_date(date_base)
		&& (date->u32_value >= date_base->u32_value))
	{
		return dt_days_elapsed_from_base(date, date_base);
	}

	return GKT_EPARAM;
}

static const gkt_date_time_s sc_dt_base = 
		GKT_DATE_TIME(GKT_DT_YEAR_BASE, GKT_DT_MONTH_JAN, 1, 0, 0, 0);
__WEAK int gkt_dt_seconds_elapsed_from_base(
				const gkt_date_time_s *dt, 
				const gkt_date_time_s *dt_base,
				uint32_t *seconds_elapsed)
{
	uint32_t days, seconds;
	int retval;

	if (!dt_base)
		dt_base = &sc_dt_base;

	if (dt && gkt_dt_check_date(&dt->date) 
		&& gkt_dt_check_time(&dt->time)
		&& gkt_dt_check_date(&dt_base->date) 
		&& gkt_dt_check_time(&dt_base->time)
		&& ((dt->date.u32_value > dt_base->date.u32_value)
				|| ((dt->date.u32_value == dt_base->date.u32_value)
					&& (dt->time.u32_value >= dt_base->time.u32_value)))
		&& seconds_elapsed)
	{
		seconds = dt_seconds_elapsed_of_day(&dt->time)
					- dt_seconds_elapsed_of_day(&dt_base->time);
		if (dt->date.u32_value > dt_base->date.u32_value) {
			days = dt_days_elapsed_from_base(&dt->date, &dt_base->date);
			seconds += days * GKT_DT_DAY_SECONDS;
		}
		*seconds_elapsed = seconds;

		retval = GKT_SUCCESS;
	}
	else
		retval = GKT_EPARAM;

	return retval;
}

__WEAK uint32_t gkt_dt_get_week(const gkt_date_u *date)
{
	uint32_t days;
	int week;

	if (date && gkt_dt_check_date(date)) {
		days = dt_days_elapsed_from_base(date, NULL);
		week = GKT_DT_WEEK_BASE + (days % GKT_DT_WEEK_DAYS);
		if (week > GKT_DT_WEEK_SUN)
			week -= GKT_DT_WEEK_SUN;
	}
	else
		week = 0;

	return week;
}

__WEAK int gkt_dt_localtime(gkt_date_time_s *dt, uint32_t seconds)
{
	uint32_t seconds_tmp, days;

	seconds += (DT_TIME_ZONE*GKT_DT_HOUR_SECONDS);

	if (dt && gkt_dt_check_date(&sc_dt_base.date)
		&& gkt_dt_check_time(&sc_dt_base.time))
	{
		dt->date.u32_value = sc_dt_base.date.u32_value;
		dt->time.u32_value = sc_dt_base.time.u32_value;
		dt->time.reserved = 0;

		if (dt->time.u32_value > 0)
		{
			seconds_tmp = (dt->time.hour * GKT_DT_HOUR_SECONDS)
				+ (dt->time.minute * GKT_DT_MINUTE_SECONDS) + dt->time.second;
			seconds_tmp = GKT_DT_DAY_SECONDS - seconds_tmp;
			if (seconds_tmp < seconds)
			{
				dt->time.u32_value = 0;
				dt_add_days(&dt->date, 1);
				seconds -= seconds_tmp;
			}
			else
			{
				gkt_dt_add_seconds(dt, seconds);
				return GKT_SUCCESS;
			}
		}
		if (seconds >= GKT_DT_DAY_SECONDS)
		{
			days = seconds / GKT_DT_DAY_SECONDS;
			seconds %= GKT_DT_DAY_SECONDS;
			dt_add_days(&dt->date, days);
		}
		if (seconds > 0)
			gkt_dt_add_seconds(dt, seconds);

		return GKT_SUCCESS;
	}

	return GKT_EPARAM;
}


